package de.mmis.core.tuplespace;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeoutException;

import org.apache.log4j.Logger;

import de.mmis.core.base.abstracttree.InnerNode;
import de.mmis.core.base.abstracttree.LeafNode;
import de.mmis.core.base.abstracttree.Tree;
import de.mmis.core.base.event.Observable;
import de.mmis.core.base.event.Observer;
import de.mmis.core.tuplespace.Notification.Command;

/**
 * extended map allowing to use multiple keys for a value and multiple values
 * for a key.
 * 
 * @author Christian Koch
 * 
 */
public class TuplespaceMultikeyMap extends TuplespaceContainer implements
		Observable<TuplespaceEvent> {

	private final static Logger LOGGER = Logger
			.getLogger(TuplespaceMultikeyMap.class);

	private static final String MAP_ELEM_NAME_SUFFIX = "-map-elem";
	private static final String MAP_NAME_SUFFIX = "-map";
	private static final String MAP_NAME_PREFIX = ":";

	private final Tuplespace space;
	private final String name;
	private final LeafNode mapName;
	private final LeafNode mapElemName;
	private final Tree id;
	private final InnerNode mapTuple;
	private final InnerNode mapWriteMutexTuple;

	private long maxNumKeys = -1;

	private final ArrayList<Observer<TuplespaceEvent>> observers;

	/**
	 * creates new tuplespace map
	 * 
	 * @param space
	 *            tuplespace client connected to used server
	 * @param name
	 *            map name (combined name and id should be unique)
	 * @param id
	 *            object id map belongs to (combined name and id should be
	 *            unique)
	 * @throws TuplespaceException
	 */
	public TuplespaceMultikeyMap(Tuplespace space, String name, Tree id)
			throws TuplespaceException {
		this(space, name, id, DEFAULT_TIMEOUT, DEFAULT_LEASETIME);
	}

	/**
	 * creates new tuplespace map, that destroy itself when not used for a
	 * specific time
	 * 
	 * @param space
	 *            tuplespace client connected to used server
	 * @param name
	 *            map name (combined name and id should be unique)
	 * @param id
	 *            object id map belongs to (combined name and id should be
	 *            unique)
	 * @param timeout
	 *            time to live between two consecutive insert or delete
	 *            operations
	 * @throws TuplespaceException
	 */
	public TuplespaceMultikeyMap(Tuplespace space, String name, Tree id,
			long timeout) throws TuplespaceException {
		this(space, name, id, timeout, DEFAULT_LEASETIME);
	}

	/**
	 * creates new tuplespace map, that destroy itself when not used for a
	 * specific time
	 * 
	 * @param space
	 *            tuplespace client connected to used server
	 * @param name
	 *            map name (combined name and id should be unique)
	 * @param id
	 *            object id map belongs to (combined name and id should be
	 *            unique)
	 * @param timeout
	 *            default time span to wait trying to take map tuple
	 * @param leaseTime
	 *            time to live between two consecutive insert or delete
	 *            operations
	 * @throws TuplespaceException
	 */
	public TuplespaceMultikeyMap(Tuplespace space, String name, Tree id,
			long timeout, long leaseTime) throws TuplespaceException {
		super(timeout, leaseTime);
		this.space = space;
		this.name = name;
		this.id = id;
		this.mapName = new LeafNode(
				(MAP_NAME_PREFIX + this.name + MAP_NAME_SUFFIX));
		this.mapElemName = new LeafNode(
				(MAP_NAME_PREFIX + this.name + MAP_ELEM_NAME_SUFFIX));

		this.mapTuple = new InnerNode(this.mapName, this.id);
		this.mapWriteMutexTuple = new InnerNode(this.mapName, this.id,
				new LeafNode(":write-mutex"));

		observers = new ArrayList<Observer<TuplespaceEvent>>();

		Notification notification = new Notification(Command.WRITE,
				mapWriteMutexTuple);
		space.notify(notification);

		space.addObserver(new Observer<TuplespaceEvent>() {
			@Override
			public void notify(Observable<? extends TuplespaceEvent> sender,
					TuplespaceEvent event) {
				if (mapWriteMutexTuple.equals(event.getTemplate()))
					fireEvent(event);
			}
		});
	}

	/**
	 * initializes the map tuple (not needed if already exists, needs to be done
	 * before any map operations can be executed)
	 * 
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 */
	public void initMap() throws TuplespaceException {
		deinitMap(0);

		space.write(mapTuple, leaseTime);
		space.write(mapWriteMutexTuple, leaseTime);
	}

	/**
	 * checks if map tuple is present on server, using default timeout
	 * 
	 * @return true, if map tuple is present, else false
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 */
	public boolean isPresent() throws TuplespaceException {
		return isPresent(getTimeout());
	}

	/**
	 * checks if map tuple is present on server
	 * 
	 * @param timeout
	 *            time span to wait for map tuple to appear. If set to 0, a
	 *            single readIfExists check will be made.
	 * @return true, if map tuple is present, else false
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 */
	public boolean isPresent(long timeout) throws TuplespaceException {
		if (timeout <= 0)
			return space.readIfExists(mapTuple) != null;
		if (space.read(mapTuple, timeout) == null)
			return false;
		return true;
	}

	/**
	 * deletes the map tuple and all elements, using default timeout
	 * 
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 */
	public void deinitMap() throws TuplespaceException {
		deinitMap(getTimeout());
	}

	/**
	 * deletes the map tuple and all elements
	 * 
	 * @param timeout
	 *            time span to wait trying to remove map tuple. If set to 0, a
	 *            single takeIfExists will be made
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 */
	public void deinitMap(long timeout) throws TuplespaceException {
		if (timeout <= 0)
			space.takeIfExists(mapTuple);
		else
			space.take(mapTuple, timeout);

		if (timeout <= 0)
			space.takeIfExists(mapWriteMutexTuple);
		else
			space.take(mapWriteMutexTuple, timeout);

		for (long numKeys = 0; numKeys < maxNumKeys; numKeys++) {
			InnerNode temp = new InnerNode(mapElemName, id);
			for (int i = 0; i < numKeys; i++)
				temp.add(new LeafNode("?"));
			temp.add(new LeafNode("?"));

			while (space.takeIfExists(temp) != null) {
				// take until empty
			}
		}
	}

	/**
	 * create or change value for specified key, using default timeout and
	 * leaseTime
	 * 
	 * @param keys
	 *            key to set value for
	 * @param value
	 *            value to set
	 * @return previous value of this element, or null, if it did not exist
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public Tree setValue(Tree value, Tree... keys) throws TuplespaceException,
			TimeoutException {
		return setValue(value, getTimeout(), leaseTime, keys);
	}

	/**
	 * create or change value for specified key, using default leaseTime
	 * 
	 * @param keys
	 *            key to set value for
	 * @param value
	 *            value to set
	 * @param timeout
	 *            time span to wait trying to remove map tuple
	 * @return previous value of this element, or null, if it did not exist
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public Tree setValue(Tree value, long timeout, Tree... keys)
			throws TuplespaceException, TimeoutException {
		return setValue(value, timeout, leaseTime, keys);
	}

	/**
	 * create or change value for specified key
	 * 
	 * @param keys
	 *            key to set value for
	 * @param value
	 *            value to set
	 * @param timeout
	 *            time span to wait trying to remove map tuple
	 * @param leaseTime
	 *            time to live for set element
	 * @return previous value of this element, or null, if it did not exist
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public Tree setValue(Tree value, long timeout, long leaseTime, Tree... keys)
			throws TuplespaceException, TimeoutException {
		InnerNode mapTuple = (InnerNode) space.take(this.mapTuple, timeout);
		if (mapTuple == null)
			throw new TimeoutException("map tuple take timed out");

		InnerNode mapWriteMutexTuple = (InnerNode) space
				.takeIfExists(this.mapWriteMutexTuple);
		if (mapWriteMutexTuple == null) {
			LOGGER.warn("map write mutex tuple missing on locked map "
					+ mapTuple + "; generating new one");
			mapWriteMutexTuple = this.mapWriteMutexTuple;
		}

		InnerNode temp = new InnerNode(mapElemName, id);
		for (Tree key : keys)
			temp.add(key);
		temp.add(new LeafNode("?"));

		// ret will get the last non-null value if any
		Tree takeExp = null, ret;
		do {
			ret = takeExp;
			takeExp = space.takeIfExists(temp);
		} while (takeExp != null);

		if (ret != null)
			ret = ((InnerNode) ret).getSubTree(temp.getNumberOfSubTrees() - 1);

		temp.setSubTree(temp.getNumberOfSubTrees() - 1, value);
		space.write(temp, leaseTime);

		maxNumKeys = Math.max(maxNumKeys, keys.length);

		space.write(mapWriteMutexTuple, this.leaseTime);
		space.write(mapTuple, this.leaseTime);

		return ret;
	}

	public void addValue(Tree value, Tree... keys) throws TuplespaceException,
			TimeoutException {
		addValue(value, DEFAULT_TIMEOUT, DEFAULT_LEASETIME, keys);
	}

	public void addValue(Tree value, long timeout, Tree... keys)
			throws TuplespaceException, TimeoutException {
		addValue(value, timeout, DEFAULT_LEASETIME, keys);
	}

	public void addValue(Tree value, long timeout, long leaseTime, Tree... keys)
			throws TuplespaceException, TimeoutException {
		InnerNode mapTuple = (InnerNode) space.take(this.mapTuple, timeout);
		if (mapTuple == null)
			throw new TimeoutException("map tuple take timed out");

		InnerNode mapWriteMutexTuple = (InnerNode) space
				.takeIfExists(this.mapWriteMutexTuple);
		if (mapWriteMutexTuple == null) {
			LOGGER.warn("map write mutex tuple missing on locked map "
					+ mapTuple + "; generating new one");
			mapWriteMutexTuple = this.mapWriteMutexTuple;
		}

		InnerNode temp = new InnerNode(mapElemName, id);
		for (Tree key : keys)
			temp.add(key);
		temp.add(value);

		while (space.takeIfExists(temp) != null) {/* remove all equal elements */
		}

		space.write(temp, leaseTime);

		maxNumKeys = Math.max(maxNumKeys, keys.length);

		space.write(mapWriteMutexTuple, this.leaseTime);
		space.write(mapTuple, this.leaseTime);
	}

	/**
	 * get value associated with given key, using default timeout
	 * 
	 * @param keys
	 *            key to find value for
	 * @return value of given key, or null, if element does not exist
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public Tree getValue(Tree... keys) throws TuplespaceException,
			TimeoutException {
		return getValue(getTimeout(), keys);
	}

	/**
	 * get value associated with given key
	 * 
	 * @param keys
	 *            key to find value for
	 * @param timeout
	 *            time span to wait trying to remove map tuple
	 * @return value of given key, or null, if element does not exist
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public Tree getValue(long timeout, Tree... keys)
			throws TuplespaceException, TimeoutException {
		InnerNode mapTuple = (InnerNode) space.take(this.mapTuple, timeout);
		if (mapTuple == null)
			throw new TimeoutException("map tuple take timed out");

		InnerNode temp = new InnerNode(mapElemName, id);
		for (Tree key : keys)
			temp.add(key);
		temp.add(new LeafNode("?"));

		Tree ret = space.readIfExists(temp);

		space.write(mapTuple, leaseTime);

		if (ret == null)
			return ret;
		return ((InnerNode) ret).getSubTree(temp.getNumberOfSubTrees() - 1);
	}

	public Tree[] getAllValues(Tree... keys) throws TuplespaceException,
			TimeoutException {
		return getAllValues(DEFAULT_TIMEOUT, keys);
	}

	public Tree[] getAllValues(long timeout, Tree... keys)
			throws TuplespaceException, TimeoutException {
		InnerNode mapTuple = (InnerNode) space.take(this.mapTuple, timeout);
		if (mapTuple == null)
			throw new TimeoutException("map tuple take timed out");

		InnerNode temp = new InnerNode(mapElemName, id);
		for (Tree key : keys)
			temp.add(key);
		temp.add(new LeafNode("?"));

		ArrayList<Tree> scanRet = space.scan(temp);

		space.write(mapTuple, leaseTime);

		Tree[] ret = new Tree[scanRet.size()];
		for (int i = 0; i < ret.length; i++)
			ret[i] = ((InnerNode) scanRet.get(i)).getSubTree(temp
					.getNumberOfSubTrees() - 1);

		return ret;
	}

	/**
	 * get map containing all entries with keys which begin with the specified
	 * S-Expressions, followed by a single wildcard (which will become the key
	 * of the returned map), mapped to its corresponding value. Note that if
	 * more than one value exists for a single key, any of the values may be
	 * chosen.
	 * 
	 * @param keys
	 *            key to find map for
	 * @return resulting map
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public Map<Tree, Tree> getKeyValueMap(Tree... keys)
			throws TuplespaceException, TimeoutException {
		return getKeyValueMap(getTimeout(), keys);
	}

	/**
	 * get map containing all entries with keys which begin with the specified
	 * S-Expressions, followed by a single wildcard (which will become the key
	 * of the returned map), mapped to its corresponding value. Note that if
	 * more than one value exists for a single key, any of the values may be
	 * chosen.
	 * 
	 * @param keys
	 *            key to find map for
	 * @param timeout
	 *            time span to wait trying to remove map tuple
	 * @return resulting map
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public Map<Tree, Tree> getKeyValueMap(long timeout, Tree... keys)
			throws TuplespaceException, TimeoutException {
		InnerNode mapTuple = (InnerNode) space.take(this.mapTuple, timeout);
		if (mapTuple == null)
			throw new TimeoutException("map tuple take timed out");

		InnerNode temp = new InnerNode(mapElemName, id);
		for (Tree key : keys)
			temp.add(key);
		temp.add(new LeafNode("?")); // last key SExpression that will be the
										// key of the returned map
		temp.add(new LeafNode("?")); // the value

		ArrayList<Tree> scanRet = space.scan(temp);

		space.write(mapTuple, leaseTime);

		Map<Tree, Tree> ret = new HashMap<Tree, Tree>();

		for (int i = 0; i < scanRet.size(); i++) {
			int size = ((InnerNode) scanRet.get(i)).getNumberOfSubTrees();
			ret.put(((InnerNode) scanRet.get(i)).getSubTree(size - 2),
					((InnerNode) scanRet.get(i)).getSubTree(size - 1));
		}

		return ret;
	}

	/**
	 * Used to generate a set of keys at the x-th position.
	 * 
	 * @param keyNum
	 *            the number to the desired keys
	 * @param totalKeys
	 *            the overall number of keys
	 * @return the key set
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public Set<Tree> getSingleKeySet(int keyNum, int totalKeys)
			throws TuplespaceException, TimeoutException {
		return getSingleKeySet(getTimeout(), keyNum, totalKeys);
	}

	/**
	 * Used to generate a set of keys at the x-th position.
	 * 
	 * @param timeout
	 *            time span to wait trying to remove map tuple
	 * @param keyNum
	 *            the number to the desired keys
	 * @param totalKeys
	 *            the overall number of keys
	 * @return the key set
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public Set<Tree> getSingleKeySet(long timeout, int keyNum, int totalKeys)
			throws TuplespaceException, TimeoutException {
		InnerNode mapTuple = (InnerNode) space.take(this.mapTuple, timeout);
		if (mapTuple == null)
			throw new TimeoutException("map tuple take timed out");

		InnerNode temp = new InnerNode(mapElemName, id);
		for (int i = 0; i < totalKeys; ++i)
			temp.add(new LeafNode("?"));
		temp.add(new LeafNode("?")); // the value

		ArrayList<Tree> scanRet = space.scan(temp);

		space.write(mapTuple, leaseTime);

		Set<Tree> ret = new HashSet<Tree>();

		for (int i = 0; i < scanRet.size(); i++) {
			InnerNode size = (InnerNode) scanRet.get(i);
			ret.add(size.getSubTree(keyNum + 2));
		}

		return ret;
	}

	/**
	 * remove element with given key, using default timeout
	 * 
	 * @param keys
	 *            key to delete
	 * @return value of given key, or null, if element did not exist
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public Tree removeKey(Tree... keys) throws TuplespaceException,
			TimeoutException {
		return removeKey(getTimeout(), keys);
	}

	/**
	 * remove element with given key
	 * 
	 * @param keys
	 *            key to delete
	 * @param timeout
	 *            time span to wait trying to remove map tuple
	 * @return value of given key, or null, if element did not exist
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public Tree removeKey(long timeout, Tree... keys)
			throws TuplespaceException, TimeoutException {
		InnerNode mapTuple = (InnerNode) space.take(this.mapTuple, timeout);
		if (mapTuple == null)
			throw new TimeoutException("map tuple take timed out");

		InnerNode mapWriteMutexTuple = (InnerNode) space
				.takeIfExists(this.mapWriteMutexTuple);
		if (mapWriteMutexTuple == null) {
			LOGGER.warn("map write mutex tuple missing on locked map "
					+ mapTuple + "; generating new one");
			mapWriteMutexTuple = this.mapWriteMutexTuple;
		}

		InnerNode temp = new InnerNode(mapElemName, id);
		for (Tree key : keys)
			temp.add(key);
		temp.add(new LeafNode("?"));

		Tree ret = space.takeIfExists(temp);
		if (ret != null)
			ret = ((InnerNode) ret).getSubTree(3);

		space.write(mapWriteMutexTuple, leaseTime);
		space.write(mapTuple, leaseTime);

		return ret;
	}

	/**
	 * removes all elements in map that have specified number of keys, using
	 * default timeout
	 * 
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public void removeAll(long numKeys) throws TuplespaceException,
			TimeoutException {
		removeAll(numKeys, getTimeout());
	}

	/**
	 * removes all elements in map
	 * 
	 * @param timeout
	 *            time span to wait trying to remove map tuple
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public void removeAll(long numKeys, long timeout)
			throws TuplespaceException, TimeoutException {
		InnerNode mapTuple = (InnerNode) space.take(this.mapTuple, timeout);
		if (mapTuple == null)
			throw new TimeoutException("map tuple take timed out");

		InnerNode mapWriteMutexTuple = (InnerNode) space
				.takeIfExists(this.mapWriteMutexTuple);
		if (mapWriteMutexTuple == null) {
			LOGGER.warn("map write mutex tuple missing on locked map "
					+ mapTuple + "; generating new one");
			mapWriteMutexTuple = this.mapWriteMutexTuple;
		}

		InnerNode temp = new InnerNode(mapElemName, id);
		for (int i = 0; i < numKeys; i++)
			temp.add(new LeafNode("?"));
		temp.add(new LeafNode("?"));

		while (space.takeIfExists(temp) != null) {
			// take until empty
		}

		space.write(mapWriteMutexTuple, leaseTime);
		space.write(mapTuple, leaseTime);
	}

	protected void fireEvent(TuplespaceEvent event) {
		synchronized (observers) {
			for (Observer<TuplespaceEvent> observer : observers) {
				observer.notify(this, event);
			}
		}
	}

	@Override
	public void addObserver(Observer<TuplespaceEvent> observer) {
		synchronized (observers) {
			observers.add(observer);
		}
	}

	@Override
	public void removeObserver(Observer<TuplespaceEvent> observer) {
		synchronized (observers) {
			observers.remove(observer);
		}
	}
}
